// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.DataProtection.Managed;

namespace Microsoft.AspNetCore.DataProtection.SP800_108
{
    internal static class ManagedSP800_108_CTR_HMACSHA512
    {
        public static void DeriveKeys(byte[] kdk, ArraySegment<byte> label, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
        {
            // make copies so we can mutate these local vars
            var outputOffset = output.Offset;
            var outputCount = output.Count;

            using (var prf = prfFactory(kdk))
            {
                // See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
                var prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Count + 1 /* 0x00 */ + context.Count + sizeof(uint) /* [K]_2 */)];

                // Copy [L]_2 to prfInput since it's stable over all iterations
                uint outputSizeInBits = (uint)checked((int)outputCount * 8);
                prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
                prfInput[prfInput.Length - 3] = (byte)(outputSizeInBits >> 16);
                prfInput[prfInput.Length - 2] = (byte)(outputSizeInBits >> 8);
                prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits);

                // Copy label and context to prfInput since they're stable over all iterations
                Buffer.BlockCopy(label.Array!, label.Offset, prfInput, sizeof(uint), label.Count);
                Buffer.BlockCopy(context.Array!, context.Offset, prfInput, sizeof(int) + label.Count + 1, context.Count);

                var prfOutputSizeInBytes = prf.GetDigestSizeInBytes();
                for (uint i = 1; outputCount > 0; i++)
                {
                    // Copy [i]_2 to prfInput since it mutates with each iteration
                    prfInput[0] = (byte)(i >> 24);
                    prfInput[1] = (byte)(i >> 16);
                    prfInput[2] = (byte)(i >> 8);
                    prfInput[3] = (byte)(i);

                    // Run the PRF and copy the results to the output buffer
                    var prfOutput = prf.ComputeHash(prfInput);
                    CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
                    var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);
                    Buffer.BlockCopy(prfOutput, 0, output.Array!, outputOffset, numBytesToCopyThisIteration);
                    Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it

                    // adjust offsets
                    outputOffset += numBytesToCopyThisIteration;
                    outputCount -= numBytesToCopyThisIteration;
                }
            }
        }

        public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
        {
            var combinedContext = new byte[checked(contextHeader.Length + context.Count)];
            Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
            Buffer.BlockCopy(context.Array!, context.Offset, combinedContext, contextHeader.Length, context.Count);
            DeriveKeys(kdk, label, new ArraySegment<byte>(combinedContext), prfFactory, output);
        }
    }
}
